---
title: "8. Add a details view"
---

In this section, you'll write a second GraphQL query that requests details about a single launch.

## Create the details query

Create a new GraphQL query named `LaunchDetails.graphql`.

As you did for `$cursor`, add a variable named `id`. Notice that this variable is a non-optional type this time. You won't be able to pass `null` like you can for `$cursor`.

Because it's a details view, request the `LARGE` size for the missionPatch. Also request the rocket type and name:

```graphql title="app/src/main/graphql/LaunchDetails.graphql"
query LaunchDetails($id: ID!) {
  launch(id: $id) {
    id
    site
    mission {
      name
      missionPatch(size: LARGE)
    }
    rocket {
      name
      type
    }
    isBooked
  }
}
```

> Remember you can always experiment in Studio Explorer and check out the left sidebar for a list of fields that are available.

## Execute the query and update the UI

In `LaunchDetails.kt`, declare `response` and a `LaunchedEffect` to execute the query:

```kotlin title="app/src/main/java/com/example/rocketreserver/LaunchDetails.kt"
@Composable
fun LaunchDetails(launchId: String) {
    var response by remember { mutableStateOf<ApolloResponse<LaunchDetailsQuery.Data>?>(null) }
    LaunchedEffect(Unit) {
        response = apolloClient.query(LaunchDetailsQuery(launchId)).execute()
    }
```

Use the response in the UI. Here too we'll use Coil's `AsyncImage` for the patch:

```kotlin title="app/src/main/java/com/example/rocketreserver/LaunchDetails.kt"
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            // Mission patch
            AsyncImage( // highlight-line
                modifier = Modifier.size(160.dp, 160.dp),
                model = response?.data?.launch?.mission?.missionPatch,
                placeholder = painterResource(R.drawable.ic_placeholder),
                error = painterResource(R.drawable.ic_placeholder),
                contentDescription = "Mission patch"
            )

            Spacer(modifier = Modifier.size(16.dp))

            Column {
                // Mission name
                Text(
                    style = MaterialTheme.typography.headlineMedium,
                    text = response?.data?.launch?.mission?.name ?: "" // highlight-line
                )

                // Rocket name
                Text(
                    modifier = Modifier.padding(top = 8.dp),
                    style = MaterialTheme.typography.headlineSmall,
                    text = response?.data?.launch?.rocket?.name?.let { "🚀 $it" } ?: "", // highlight-line
                )

                // Site
                Text(
                    modifier = Modifier.padding(top = 8.dp),
                    style = MaterialTheme.typography.titleMedium,
                    text = response?.data?.launch?.site ?: "", // highlight-line
                )
            }
        }

```

## Show a loading state

Since `response` is initialized to `null`, you can use this as an indication that the result has not been received yet.

To structure the code a bit, extract the details UI into a separate function that takes the response as a parameter:

```kotlin title="app/src/main/java/com/example/rocketreserver/LaunchDetails.kt"
@Composable
private fun LaunchDetails(response: ApolloResponse<LaunchDetailsQuery.Data>) {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
```
Now in the original `LaunchDetails` function, check if `response` is `null` and show a loading indicator if it is, otherwise call the new function with the response:

```kotlin title="app/src/main/java/com/example/rocketreserver/LaunchDetails.kt"
@Composable
fun LaunchDetails(launchId: String) {
    var response by remember { mutableStateOf<ApolloResponse<LaunchDetailsQuery.Data>?>(null) }
    LaunchedEffect(Unit) {
        response = apolloClient.query(LaunchDetailsQuery(launchId)).execute()
    }
    if (response == null) { // highlight-line
        Loading() // highlight-line
    } else { // highlight-line
        LaunchDetails(response!!) // highlight-line
    }
}
```

## Handle errors

As you execute your query, different types of errors can happen:

* Fetch errors: connectivity issues, HTTP errors, JSON parsing errors, etc... In this case, `response.exception` contains an `ApolloException`. Both `response.data` and `response.errors` is null.
* GraphQL [request errors](https://spec.graphql.org/draft/#request-error): in this case, `response.errors` contains the GraphQL errors. `response.data` is null.
* GraphQL [field errors](https://spec.graphql.org/draft/#field-error): in this case, `response.errors` contains the GraphQL errors. `response.data` contains partial data.

Let's handle the first two for now: fetch errors and GraphQL request errors. 

First let's create a `LaunchDetailsState` sealed interface that will hold the possible states of the UI:

```kotlin title="app/src/main/java/com/example/rocketreserver/LaunchDetails.kt"
private sealed interface LaunchDetailsState {
    object Loading : LaunchDetailsState
    data class Error(val message: String) : LaunchDetailsState
    data class Success(val data: LaunchDetailsQuery.Data) : LaunchDetailsState
}
```

Then, in `LaunchDetails`, examine the response returned by `execute` and map it to a `State`:

```kotlin title="app/src/main/java/com/example/rocketreserver/LaunchDetails.kt"
@Composable
fun LaunchDetails(launchId: String) {
    var state by remember { mutableStateOf<LaunchDetailsState>(Loading) }
    LaunchedEffect(Unit) {
        val response = apolloClient.query(LaunchDetailsQuery(launchId)).execute()
        state = when {
            response.data != null -> {
              // Handle (potentially partial) data
              LaunchDetailsState.Success(response.data!!)
            }
            else -> {
              LaunchDetailsState.Error("Oh no... An error happened.")
            }
        }
    }
    // Use the state
```

Now use the state to show the appropriate UI:

```kotlin title="app/src/main/java/com/example/rocketreserver/LaunchDetails.kt"
    
    // Use the state
    when (val s = state) {
        Loading -> Loading()
        is Error -> ErrorMessage(s.message)
        is Success -> LaunchDetails(s.data)
    }
}
```

Enable airplane mode before clicking the details of a launch. You should see this:

<img src="images/error_generic.png" alt="Oh no" class="screenshot" width="300"/>

This is good! 

This method handles fetch and GraphQL request errors but ignores GraphQL field errors. 

If a GraphQL field error happens, the corresponding Kotlin property is `null` and an error is present in `response.errors`: your response contains partial data!

Handling this partial data in your UI code can be complicated. You can handle them earlier by looking at `response.errors`. 

## Handle partial data

To handle GraphQL field errors globally and make sure the returned data is not partial, use `response.errors`:

```kotlin title="app/src/main/java/com/example/rocketreserver/LaunchDetails.kt"
state = when {
  response.errors.orEmpty().isNotEmpty() -> {
    // GraphQL error
    LaunchDetailsState.Error(response.errors!!.first().message) // highlight-line
  }
  response.exception is ApolloNetworkException -> {
    // Network error
    LaunchDetailsState.Error("Please check your network connectivity.")
  }
  response.data != null -> {
    // data (never partial)
    LaunchDetailsState.Success(response.data!!)
  }
  else -> {
    // Another fetch error, maybe a cache miss?
    // Or potentially a non-compliant server returning data: null without an error
    LaunchDetailsState.Error("Oh no... An error happened.")
  }
}
```

`response.errors` contains details about any errors that occurred. Note that this code also null-checks `response.data!!`. In theory, a server should not set `response.data == null` and `response.hasErrors == false` at the same time, but the type system cannot guarantee this.

To trigger a GraphQL field error, replace `LaunchDetailsQuery(launchId)` with `LaunchDetailsQuery("invalidId")`. Disable airplane mode and select a launch. The server will send this response:

```json title="(error)"
{
  "errors": [
    {
      "message": "Cannot read property 'flight_number' of undefined",
      "locations": [
        {
          "line": 1,
          "column": 32
        }
      ],
      "path": [
        "launch"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR"
      }
    }
  ],
  "data": {
    "launch": null
  }
}
```

<img src="images/error_application.png" alt="Oh no" class="screenshot" width="300"/>

This is all good! You can use the `errors` field to add more advanced error management.

Restore the correct launch ID: `LaunchDetailsQuery(launchId)` before displaying details.

## Handle the **Book now** button

To book a trip, the user must be logged in. If the user is not logged in, clicking the **Book now** button should open the login screen.

First let's pass a lambda to `LaunchDetails` to take care of navigation:

```kotlin title="app/src/main/java/com/example/rocketreserver/LaunchDetails.kt"
@Composable
fun LaunchDetails(launchId: String, navigateToLogin: () -> Unit) { // highlight-line
```

The lambda should be declared in MainActivity, where the navigation is handled:

```kotlin title="app/src/main/java/com/example/rocketreserver/MainActivity.kt"
composable(route = "${NavigationDestinations.LAUNCH_DETAILS}/{${NavigationArguments.LAUNCH_ID}}") { navBackStackEntry ->
    LaunchDetails(
        launchId = navBackStackEntry.arguments!!.getString(NavigationArguments.LAUNCH_ID)!!,
        navigateToLogin = { // highlight-line
          navController.navigate(NavigationDestinations.LOGIN) // highlight-line
        } // highlight-line
    )
}
```
Next, go back to `LaunchDetails.kt` and replace the `TODO` with a call to a function where we handle the button click: 

```kotlin title="app/src/main/java/com/example/rocketreserver/LaunchDetails.kt"
onClick = {
    onBookButtonClick(
        launchId = data.launch?.id ?: "",
        isBooked = data.launch?.isBooked == true,
        navigateToLogin = navigateToLogin
    )
}
```

```kotlin title="app/src/main/java/com/example/rocketreserver/LaunchDetails.kt"
private fun onBookButtonClick(launchId: String, isBooked: Boolean, navigateToLogin: () -> Unit): Boolean {
    if (TokenRepository.getToken() == null) {
        navigateToLogin()
        return false
    }

    if (isBooked) {
        // TODO Cancel booking
    } else {
        // TODO Book
    }
    return false
}
```

`TokenRepository` is a helper class that handles saving/retrieving a user token in [EncryptedSharedPreference](https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences). We will use it to store the user token when logging in.

Returning a boolean will be useful later to update the UI depending on whether the execution happened or not.

## Test the button

Hit **Run**. Your screen should look like this:

<img src="images/details.png" alt="Details" class="screenshot" width="300"/>

Right now, you aren't logged in, so you won't be able to book a trip and clicking will always navigate to the login screen.

Next, you will [write your first mutation](09-write-your-first-mutation) to log in to the backend.
